﻿#region namespaces

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Timers;
using System.Windows;
using System.Windows.Threading;
using Db4objects.Db4o;
using Db4objects.Db4o.Activation;
using Db4objects.Db4o.Events;
using Db4objects.Db4o.Ext;
using Db4objects.Db4o.Linq;
using Db4objects.Db4o.TA;
using ZOIL.DataModel;

#endregion

namespace ZOIL.DataBackend.Db4o
{
    /// <summary>
    ///     Singleton Class. Core of the Databackend on client side. Is used to retrieve and store objects from/into the database.
    /// </summary>
    public class Database
    {
        #region Private Fields

        internal static readonly int UpdateDepth = 9;

        private TaskQueue<object> _addedObjects;
        private TaskQueue<object> _updatedObjects;
        private TaskQueue<object> _deletedObjects;

        private readonly ReaderWriterLockSlim _cacheRW;

        private readonly ReaderWriterLockSlim _commitRW;

        /// <summary>
        ///     a timer that enables continuous committing at a certain interval
        /// </summary>
        private readonly System.Timers.Timer _commitTimer;

        /// <summary>
        ///     reference to the database client
        /// </summary>
        private Client _client;

        /// <summary>
        ///     a BackgroundWorker that triggers a commit on the database
        /// </summary>
        private BackgroundWorker _commitWorker;

        #endregion

        #region Public Properties

        /// <summary>
        ///     This is the ObjectContainer that enables us to query for Objects in the database and commit
        ///     changes to the database
        /// </summary>
        public IObjectContainer Objects
        {
            get
            {
                if (_client == null)
                {
                    return null;
                }
                //TODO: implement reconnect logic here, if _client.Objects == null
                return _client.Objects;
            }
        }

        private List<Object> _objectCache;

        public List<Object> ObjectCache
        {
            get { return _objectCache; }
        }

        public IEventRegistry EventRegistry { get; private set; }

        #endregion

        #region Singleton

        private Database()
        {
            _locallyUpdatedObjects = new List<object>();
            _commitTimer = new System.Timers.Timer();
            _cacheRW = new ReaderWriterLockSlim();
            _commitRW = new ReaderWriterLockSlim();
            CommitInterval = 500; //Default commit interval is 500 ms
        }

        private static Database _instance;

        public static Database Instance
        {
            get { return _instance ?? (_instance = new Database()); }
        }

        #endregion

        #region Change Committing

        /// <summary>
        ///     initializes the change committing by creating a timer and register the events that start the committing
        /// </summary>
        private void InitializeChangeCommitting()
        {
            _commitTimer.Elapsed += CommitTimer_Elapsed;
            _commitWorker = new BackgroundWorker();
            _commitWorker.DoWork += CommitWorker_DoWork;
            _commitTimer.Start();
        }

        /// <summary>
        ///     Event Handler of the DoWork event of the BackgroundWorker. Triggers the committing on the database
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CommitWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                CommitChanges();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        /// <summary>
        ///     event Handler of the Elapsed event of the Commit Timer. Triggers the asynch starting of the BackgroundWorker
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CommitTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            try
            {
                if (!_commitWorker.IsBusy)
                {
                    _commitWorker.RunWorkerAsync();
                }
            }
            catch (Exception)
            {
                Debug.WriteLine("Commit Worker is busy");
            }
        }

        /// <summary>
        ///     Represents the Value of the CommitTimer Interval in miliseconds
        /// </summary>
        public double CommitInterval
        {
            get { return _commitTimer.Interval; }
            set { _commitTimer.Interval = value; }
        }

        #endregion

        #region Database Management

        /// <summary>
        ///     Configures the Database and opens the connection
        /// </summary>
        public void Configure(String host, int port, String username, String password, bool automaticChangeCommitting)
        {
            _client = new Client(host, port, username, password);

            _client.Connect();

            //first rebuild object cache in order to avoid some thread race conditions
            RebuildObjectCache();

            _addedObjects = new TaskQueue<object>(5, ProcessAddedObject);
            _updatedObjects = new TaskQueue<object>(25, ProcessUpdatedObject);
            _deletedObjects = new TaskQueue<object>(5, ProcessDeletedObject);

            EventRegistry = EventRegistryFactory.ForObjectContainer(_client.Objects);

            //EventRegistry.Committed += new CommitEventHandler(EventRegistry_Committed);
            //EventRegistry.Updated += new ObjectEventHandler(EventRegistry_Updated);

            EventRegistry.Committed += OnCommitted;
            //TODO: Ergibt das einen Sinn? Update wird sowohl in Committed als auch in Updated behandelt?
            EventRegistry.Updated += OnUpdated;

            //Register event handlers for all events that can occur on the database
            //EventRegistry.Activated += new ObjectEventHandler(EventRegistry_Activated);
            //EventRegistry.Activating += new CancellableObjectEventHandler(EventRegistry_Activating);
            //EventRegistry.ClassRegistered += new ClassEventHandler(EventRegistry_ClassRegistered);
            //EventRegistry.Closing += new ObjectContainerEventHandler(EventRegistry_Closing);
            //EventRegistry.Created += new ObjectEventHandler(EventRegistry_Created);
            //EventRegistry.Creating += new CancellableObjectEventHandler(EventRegistry_Creating);
            //EventRegistry.Deactivated += new ObjectEventHandler(EventRegistry_Deactivated);
            //EventRegistry.Deactivating += new CancellableObjectEventHandler(EventRegistry_Deactivating);
            //EventRegistry.Deleted += new ObjectEventHandler(EventRegistry_Deleted);
            //EventRegistry.Deleting += new CancellableObjectEventHandler(EventRegistry_Deleting);
            //EventRegistry.Instantiated += new ObjectEventHandler(EventRegistry_Instantiated);
            //EventRegistry.QueryFinished += new QueryEventHandler(EventRegistry_QueryFinished);
            //EventRegistry.QueryStarted += new QueryEventHandler(EventRegistry_QueryStarted);

            if (automaticChangeCommitting)
            {
                InitializeChangeCommitting();
            }
        }

        #region Track Changes

        private void OnObjectLocallyChanged(object sender, EventArgs eventArgs)
        {
            Objects.Ext().Store(sender, UpdateDepth);
        }

        #endregion

        /// <summary>
        ///     does a final commit and then closes the Database
        /// </summary>
        public void Close()
        {
            _commitTimer.Stop();

            try
            {
                CommitChanges();
            }
            catch (DatabaseException dbe)
            {
                Console.WriteLine(dbe.Message);
            }

            try
            {
                _client.CloseConnection();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }

            _addedObjects.Dispose();
            _updatedObjects.Dispose();
            _deletedObjects.Dispose();
        }

        #endregion

        #region Database Events

        private readonly List<object> _locallyUpdatedObjects;
        private readonly object _localUpdateLocker = new object();

        private void OnUpdated(object sender, ObjectEventArgs args)
        {
            object updatedObject = args.Object;

            lock (_localUpdateLocker)
            {
                if (!_locallyUpdatedObjects.Contains(updatedObject))
                {
                    _locallyUpdatedObjects.Add(updatedObject);
                }
            }
        }

        private void OnCommitted(object sender, CommitEventArgs args)
        {
            foreach (IObjectInfo info in args.Added)
            {
                var o = info.GetObject();

                var db4oColl = o as Db4oCollection;
                if (db4oColl != null)
                {
                    db4oColl.OnCreated();
                }

                _addedObjects.EnqueueItem(o);
            }

            foreach (IObjectInfo info in args.Updated)
            {
                _updatedObjects.EnqueueItemIfNotInQueue(info.GetObject());
            }

            foreach (IObjectInfo info in args.Deleted)
            {
                var deletedObject = info.GetObject();
                var pc = deletedObject as IObjectLocallyChanged;
                if (pc != null)
                {
                    pc.ObjectLocallyChanged -= OnObjectLocallyChanged;
                }
                _deletedObjects.EnqueueItemIfNotInQueue(deletedObject);
            }
        }

        private void ProcessAddedObject(object addedObject)
        {
            ObjectCache.Add(addedObject);

            Objects.Activate(addedObject, 5);

            if (addedObject is IActivatable)
            {
                (addedObject as IActivatable).Activate(ActivationPurpose.Read);
            }

//            //wird immer aufgerufen
//            {
//                var noc = addedObject as INotifyObjectRemotelyCreated;
//                if (noc != null)
//                {
//                    Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(noc.OnRemotelyCreated));
//                }
//            }

            {
                var pc = addedObject as IObjectLocallyChanged;
                if (pc != null && !(addedObject is IActivatable))
                {
                    pc.ObjectLocallyChanged += OnObjectLocallyChanged;
                }
            }
        }

        private void ProcessUpdatedObject(object addedObject)
        {
            //check if the object has been updated locally
            lock (_localUpdateLocker)
            {
                if (_locallyUpdatedObjects.Contains(addedObject))
                {
                    _locallyUpdatedObjects.Remove(addedObject);
                    return;
                }
            }

            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => Objects.Ext().Refresh(addedObject, UpdateDepth)));
//            Objects.Ext().Refresh(addedObject, 2);

            var ru = addedObject as IRemoteUpdateable;
            if (ru != null)
            {
                Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(ru.OnRemoteUpdated));
//                ru.OnRemoteUpdated();
            }
        }

        private void ProcessDeletedObject(object deletedObject)
        {
        }

        #endregion

        #region Object Management

        public void RebuildObjectCache()
        {
            var objs = Objects.Ext().Query().Execute();
            var ml = new List<object>();
            foreach (var obj in objs)
            {
                ml.Add(obj);
            }
            
            List<Object> tempCache = (from object o in Objects select o).ToList();

            _cacheRW.EnterWriteLock();
            _objectCache = tempCache;
            _cacheRW.ExitWriteLock();

            foreach (object o in _objectCache)
            {
                Objects.Activate(o, 5);

                if (o is IActivatable)
                {
                    (o as IActivatable).Activate(ActivationPurpose.Read);
                }
             
                {
                    var olc = o as IObjectLocallyChanged;
                    if (olc != null && !(o is IActivatable))
                    {
                        olc.ObjectLocallyChanged += OnObjectLocallyChanged;
                    }
                }
            }
        }

        /// <summary>
        ///     explicitly stores an object in the database.
        ///     Note: no commit is performed
        /// </summary>
        /// <param name="o"></param>
        public void Store(object o)
        {
            try
            {
                Objects.Ext().Store(o, UpdateDepth);
            }
            catch (ObjectNotStorableException ex)
            {
                Console.WriteLine("Could not store object of type " + o.GetType() + ". Reason: " + ex.Message);
                throw new DatabaseException("Could not store object of type " + o.GetType(), ex);
            }
        }

        /// <summary>
        ///     explicitly stores an object in the database and commits those changes
        /// </summary>
        /// <param name="o"></param>
        public void StoreAndCommit(object o)
        {
            Store(o);
            CommitChanges();
        }

        /// <summary>
        ///     commits all unsaved changes to the database
        /// </summary>
        public void CommitChanges()
        {
            try
            {
                lock (_activationLocker)
                {
                    while (_activationQueue.Count != 0)
                    {
                        IActivatable act = _activationQueue.Dequeue();
                        act.Activate(ActivationPurpose.Write);
                    }
                }

                //if (_commitRW.IsReadLockHeld)
                //    return;

                _commitRW.EnterReadLock();
                Objects.Commit();
                _commitRW.ExitReadLock();
            }
            catch (Exception e)
            {
                throw new DatabaseException("Failed to commit. Reason: ", e);
            }
        }

        public void StoreItem(IPersistable item, IVisualProperties prop)
        {
            try
            {
                using (_commitRW.WriteLock())
                {
                    Store(item);
                    IObjectInfo info = Objects.Ext().GetObjectInfo(item);
                    var id = info.GetUUID().GetLongPart();
                    Store(prop);
                    item.DatabaseId = prop.BelongsToDatabaseId = id;
                    Store(item);
                    Store(prop);
                }
            }
            catch (Exception ex)
            {
                String message = "Could not store Object of type" + item.GetType() + ". Reason: " + ex.Message;
                Console.WriteLine(message);
                throw new DatabaseException(message);
            }
//            CommitChanges();
        }

        #endregion

        #region Object Activation

        private readonly Object _activationLocker = new object();
        private readonly Queue<IActivatable> _activationQueue = new Queue<IActivatable>();

        public void ActivateForWriting(IActivatable act)
        {
            Enqueue(act);
        }

        private void Enqueue(IActivatable act)
        {
            lock (_activationLocker)
            {
                if (_activationQueue.Contains(act))
                {
                    return;
                }
                _activationQueue.Enqueue(act);
            }
        }

        #endregion
    }
}